package com.huan.study.mybatis.plugin;

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Array;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * Mybatis 插件：记录带参数的 SQL（支持 JDK8 java.time）
 * <p>
 * 可通过 plugin properties 配置：
 * - enabled: true/false
 * - slowSqlMillis: long 毫秒
 * - timeZone: 时区字符串，例如 "Asia/Shanghai"
 * - dateTimePattern: 时间格式，例如 "yyyy-MM-dd HH:mm:ss"（默认）
 * - booleanAsNumber: true/false
 * - maskPatterns: 逗号分隔的正则，匹配参数名时掩码该参数
 */
@Intercepts({
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, org.apache.ibatis.session.RowBounds.class, org.apache.ibatis.session.ResultHandler.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, org.apache.ibatis.session.RowBounds.class, org.apache.ibatis.session.ResultHandler.class, org.apache.ibatis.cache.CacheKey.class, BoundSql.class})
})
public class MybatisSqlInterceptor implements Interceptor {
    private static final Logger logger = LoggerFactory.getLogger(MybatisSqlInterceptor.class);

    private boolean enabled = true;
    private long slowSqlMillis = Long.MAX_VALUE;
    private ZoneId zoneId = ZoneId.systemDefault();

    // 可配置的时间格式（默认）
    private String dateTimePattern = "yyyy-MM-dd HH:mm:ss";
    // 两个 formatter：一个用于 Instant/带时区的 Temporal，一个位于本地 temporal（LocalDateTime/LocalDate/LocalTime）
    private volatile DateTimeFormatter instantFormatter;
    private volatile DateTimeFormatter localFormatter;

    private boolean booleanAsNumber = true;
    private List<java.util.regex.Pattern> maskPatterns = Collections.emptyList();

    public MybatisSqlInterceptor() {
        // 初始化 formatters
        this.instantFormatter = DateTimeFormatter.ofPattern(dateTimePattern).withZone(zoneId);
        this.localFormatter = DateTimeFormatter.ofPattern(dateTimePattern);
    }

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        if (!enabled) {
            return invocation.proceed();
        }

        // 睡眠1s，模拟耗时
        TimeUnit.SECONDS.sleep(1);

        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];
        Object parameter = args.length > 1 ? args[1] : null;

        long start = System.currentTimeMillis();
        Object result = invocation.proceed();
        long elapsed = System.currentTimeMillis() - start;

        boolean isSlow = elapsed >= slowSqlMillis;
        boolean wantDebug = logger.isDebugEnabled();

        if (!isSlow && !wantDebug) {
            return result;
        }

        BoundSql boundSql = null;
        try {
            if (args.length == 6 && args[5] instanceof BoundSql) {
                boundSql = (BoundSql) args[5];
            } else {
                boundSql = ms.getBoundSql(parameter);
            }
        } catch (Exception e) {
            logger.debug("Failed to obtain BoundSql for logging", e);
        }

        String sqlWithParams;
        try {
            if (boundSql != null) {
                sqlWithParams = buildSqlEfficient(ms.getConfiguration(), boundSql);
            } else {
                sqlWithParams = "/* Unable to obtain BoundSql */";
            }
        } catch (Exception e) {
            sqlWithParams = "/* Failed to build SQL with params: " + e.getMessage() + " */";
            logger.debug("Error building SQL with params", e);
        }

        String sqlId = ms.getId();
        if (isSlow && logger.isWarnEnabled()) {
            logger.warn("Slow SQL ({} ms) id={} \n{}", elapsed, sqlId, sqlWithParams);
        } else if (wantDebug && logger.isDebugEnabled()) {
            logger.debug("SQL ({} ms) id={} \n{}", elapsed, sqlId, sqlWithParams);
        }

        return result;
    }

    // 高效替换实现：找到下一个 '?' 并替换
    private String buildSqlEfficient(Configuration configuration, BoundSql boundSql) {
        String sql = boundSql.getSql();
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        Object parameterObject = boundSql.getParameterObject();

        if (parameterMappings == null || parameterMappings.isEmpty() || parameterObject == null) {
            return beautifySql(sql);
        }

        MetaObject metaObject = configuration.newMetaObject(parameterObject);

        StringBuilder out = new StringBuilder(sql.length() + 100);
        int sqlPos = 0;
        int nextQ;
        int paramIndex = 0;

        while (paramIndex < parameterMappings.size() && (nextQ = sql.indexOf('?', sqlPos)) != -1) {
            out.append(sql, sqlPos, nextQ);

            ParameterMapping pm = parameterMappings.get(paramIndex++);
            String propertyName = pm.getProperty();
            Object value;
            if (boundSql.hasAdditionalParameter(propertyName)) {
                value = boundSql.getAdditionalParameter(propertyName);
            } else if (metaObject.hasGetter(propertyName)) {
                value = metaObject.getValue(propertyName);
            } else if (isSimpleType(parameterObject.getClass()) && parameterMappings.size() == 1) {
                value = parameterObject;
            } else {
                value = null;
            }

            String valueStr = formatParameterValueWithMask(value, propertyName);
            out.append(valueStr);

            sqlPos = nextQ + 1;
        }

        out.append(sql.substring(sqlPos));
        return beautifySql(out.toString());
    }

    // 支持参数名掩码
    private String formatParameterValueWithMask(Object value, String propertyName) {
        if (shouldMask(propertyName)) {
            return "'***'";
        }
        return formatParameterValue(value);
    }

    private boolean shouldMask(String propertyName) {
        if (maskPatterns == null || maskPatterns.isEmpty()) return false;
        if (propertyName == null) return false;
        for (java.util.regex.Pattern p : maskPatterns) {
            if (p.matcher(propertyName).find()) return true;
        }
        return false;
    }

    // 扩展：支持 java.time（JDK8+）以及旧 Date 类型
    private String formatParameterValue(Object value) {
        if (value == null) {
            return "NULL";
        }

        // 字符串与字符
        if (value instanceof String || value instanceof Character) {
            return "'" + escapeSql(value.toString()) + "'";
        }

        // JDK8 java.time 类型
        if (value instanceof Instant) {
            return "'" + instantFormatter.format((Instant) value) + "'";
        }
        if (value instanceof ZonedDateTime) {
            return "'" + instantFormatter.format(((ZonedDateTime) value).toInstant()) + "'";
        }
        if (value instanceof OffsetDateTime) {
            return "'" + instantFormatter.format(((OffsetDateTime) value).toInstant()) + "'";
        }
        if (value instanceof LocalDateTime) {
            return "'" + localFormatter.format((LocalDateTime) value) + "'";
        }
        if (value instanceof LocalDate) {
            return "'" + localFormatter.format((LocalDate) value) + "'";
        }
        if (value instanceof LocalTime) {
            return "'" + localFormatter.format((LocalTime) value) + "'";
        }
        if (value instanceof OffsetTime) {
            // 将 OffsetTime 按本地 formatter（结果仅时间部分按 pattern）
            return "'" + localFormatter.format((OffsetTime) value) + "'";
        }

        // 旧的 java.util.Date / java.sql.Timestamp
        if (value instanceof java.util.Date) {
            Instant instant = ((java.util.Date) value).toInstant();
            return "'" + instantFormatter.format(instant) + "'";
        }

        if (value instanceof Boolean) {
            return booleanAsNumber ? (((Boolean) value) ? "1" : "0") : (((Boolean) value) ? "true" : "false");
        }

        // 数组
        if (value.getClass().isArray()) {
            int len = Array.getLength(value);
            List<String> parts = new ArrayList<>(len);
            for (int i = 0; i < len; i++) {
                Object elem = Array.get(value, i);
                parts.add(formatParameterValue(elem));
            }
            return "(" + String.join(",", parts) + ")";
        }

        // Collection
        if (value instanceof Collection) {
            Collection<?> col = (Collection<?>) value;
            List<String> parts = new ArrayList<>(col.size());
            for (Object elem : col) {
                parts.add(formatParameterValue(elem));
            }
            return "(" + String.join(",", parts) + ")";
        }

        // Map
        if (value instanceof Map) {
            return "'" + escapeSql(value.toString()) + "'";
        }

        // 其它（Number, Enum 等）
        return value.toString();
    }

    private String beautifySql(String sql) {
        if (sql == null) return null;
        return sql.replaceAll("[\\s]+", " ").trim();
    }

    private String escapeSql(String input) {
        if (input == null) return null;
        return input.replace("'", "''");
    }

    private boolean isSimpleType(Class<?> clazz) {
        return clazz.isPrimitive()
                || Number.class.isAssignableFrom(clazz)
                || CharSequence.class.isAssignableFrom(clazz)
                || Date.class.isAssignableFrom(clazz)
                || Boolean.class.isAssignableFrom(clazz)
                || Enum.class.isAssignableFrom(clazz);
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    /**
     * 支持通过 <plugin> 的 property 进行配置：
     * - enabled: true/false
     * - slowSqlMillis: 毫秒（>= 此值会强制记录为 WARN）
     * - timeZone: 时区字符串，例如 "Asia/Shanghai"
     * - dateTimePattern: 时间格式，例如 "yyyy-MM-dd HH:mm:ss"
     * - booleanAsNumber: true/false
     * - maskPatterns: 逗号分隔的正则，例如 "password,.*secret.*"
     */
    @Override
    public void setProperties(Properties properties) {
        if (properties == null) return;
        String en = properties.getProperty("enabled");
        if (en != null) {
            this.enabled = Boolean.parseBoolean(en);
        }
        String slow = properties.getProperty("slowSqlMillis");
        if (slow != null) {
            try {
                this.slowSqlMillis = Long.parseLong(slow);
            } catch (NumberFormatException ignored) {
            }
        }
        String tz = properties.getProperty("timeZone");
        if (tz != null && !tz.trim().isEmpty()) {
            try {
                this.zoneId = ZoneId.of(tz.trim());
            } catch (Exception e) {
                logger.warn("Invalid timeZone property for MybatisSqlInterceptor: {}", tz);
            }
        }
        String pattern = properties.getProperty("dateTimePattern");
        if (pattern != null && !pattern.trim().isEmpty()) {
            this.dateTimePattern = pattern.trim();
        }

        // 根据最终 zoneId 和 pattern 初始化 formatter（一次性）
        try {
            this.instantFormatter = DateTimeFormatter.ofPattern(this.dateTimePattern).withZone(this.zoneId);
            this.localFormatter = DateTimeFormatter.ofPattern(this.dateTimePattern);
        } catch (Exception e) {
            // 回退到默认格式
            logger.warn("Invalid dateTimePattern '{}', fallback to default", this.dateTimePattern);
            this.dateTimePattern = "yyyy-MM-dd HH:mm:ss";
            this.instantFormatter = DateTimeFormatter.ofPattern(this.dateTimePattern).withZone(this.zoneId);
            this.localFormatter = DateTimeFormatter.ofPattern(this.dateTimePattern);
        }

        String boolAsNum = properties.getProperty("booleanAsNumber");
        if (boolAsNum != null) this.booleanAsNumber = Boolean.parseBoolean(boolAsNum);

        String mask = properties.getProperty("maskPatterns"); // e.g. "password,.*secret.*"
        if (mask != null && !mask.trim().isEmpty()) {
            List<java.util.regex.Pattern> patterns = new ArrayList<>();
            for (String s : mask.split(",")) {
                if (!s.trim().isEmpty()) {
                    patterns.add(java.util.regex.Pattern.compile(s.trim(), java.util.regex.Pattern.CASE_INSENSITIVE));
                }
            }
            this.maskPatterns = patterns;
        }
    }
}
